//
// Created by shubin on 10.04.24.
//

#pragma once

#include <GL/glut.h>
#include <vector>
//#include "SOIL/SOIL.h"
#include "pv_png/basisu_miniz.h"
#include "pv_png/pvpngreader.h"

struct Vector3f {
    GLfloat x;
    GLfloat y;
    GLfloat z;

    void assign(GLfloat _x, GLfloat _y, GLfloat _z) {
        this->x = _x;
        this->y = _y;
        this->z = _z;
    }

    void operator+=(Vector3f& value) {
        this->x += value.x;
        this->y += value.y;
        this->z += value.z;
    }
};

union Point3f {
    struct {
        GLfloat x;
        GLfloat y;
        GLfloat z;
    };
    GLfloat items[3]{};

    void operator+=(Vector3f& value) {
        this->x += value.x;
        this->y += value.y;
        this->z += value.z;
    }

    Point3f(GLfloat x, GLfloat y, GLfloat z) {
        this->x = x;
        this->y = y;
        this->z = z;
    }
};

struct RotateValue {
    int angle;
    Vector3f vector;

    void operator+=(RotateValue& value) {
        this->angle += value.angle;
        this->angle = this->angle % 360;
        this->vector += value.vector;
    }
};


class Texture {
private:
protected:
    GLuint id = 0;
    GLint width = 0;
    GLint height = 0;
public:

    GLuint get_id() const {
        return this->id;
    }

    GLint get_width() const {
        return this->width;
    }

    GLint get_height() const {
        return this->height;
    }

    GLuint load(const char * file_name) {

        uint32_t w;
        uint32_t h;
        uint32_t c;
        void * png = nullptr;
        FILE * pFile = fopen(file_name, "rb");
        fseek(pFile, 0, SEEK_END);
        int64_t filesize = ftello(pFile);
        fseek(pFile, 0, SEEK_SET);
        std::vector<uint8_t> buf((size_t)filesize);
        //pv_png::png_info info{};
        if (fread(buf.data(), 1, buf.size(), pFile) == buf.size()) {
            fclose(pFile);
            //void* load_png(const void* pImage_buf, size_t buf_size, uint32_t desired_chans, uint32_t& width, uint32_t& height, uint32_t& num_chans)
            png = pv_png::load_png(buf.data(), buf.size(),  4, w, h, c);
            //pv_png::get_png_info(buf.data(), buf.size(), info);
        }
        if (png) {
            glGenTextures(1, &this->id);
            glBindTexture(GL_TEXTURE_2D, this->id);
            GLint fmt;
            if (c==4)
                fmt = GL_RGBA;
            else
                fmt = GL_RGB;
            glTexImage2D(GL_TEXTURE_2D, 0, fmt, (GLsizei)w, (GLsizei)h, 0, fmt, GL_UNSIGNED_BYTE, png);
            glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &this->width);
            glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &this->height);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
            glBindTexture(GL_TEXTURE_2D, 0);
            free(png);
        }
        return this->id;
    }

    Texture() = default;

    ~Texture() {
        glDeleteTextures(1, &this->id);
    }
};

class Drawable{
public:
    virtual void draw(float x, float y) {
        glPushMatrix();
        glTranslatef(x, y, 0);
        this->draw();
        glPopMatrix();
    }

    virtual void draw() = 0;
};

class Textured: public Drawable {
protected:
    Texture * texture = nullptr;
public:
    Textured() {
        this->texture = new Texture();
    }
    virtual ~Textured() {
        if (this->texture)
            delete this->texture;
    }

    Texture * get_texture() {
        return this->texture;
    }

    virtual GLfloat get_width() {
        return GLfloat(this->texture->get_width());
    }

    virtual GLfloat get_height() {
        return GLfloat(this->texture->get_height());
    }
    virtual GLuint load(const char * file_name) {
        return this->texture->load(file_name);
    }
};


class Sprite: public Textured {
private:
    bool prepared = false;
    GLuint list_id{};
public:
    Sprite():Textured() {}

    void draw(float x, float y) override {
        Drawable::draw(x, y);
    }

    void draw() override {
        if (this->texture->get_id()) {
            if (!prepared) {
                list_id = glGenLists(1);
                glNewList(list_id, GL_COMPILE);
                glBindTexture(GL_TEXTURE_2D, this->texture->get_id());
                glBegin(GL_QUADS);
                glTexCoord2f(0, 0);
                glVertex2i(0, 0);
                glTexCoord2f(0, 1);
                glVertex2i(0, this->texture->get_height());
                glTexCoord2f(1, 1);
                glVertex2i(this->texture->get_width(), this->texture->get_height());
                glTexCoord2f(1, 0);
                glVertex2i(this->texture->get_width(), 0);
                glEnd();
                glBindTexture(GL_TEXTURE_2D, 0);
                glEndList();
                prepared = true;
            }
            glCallList(list_id);
        }
    }
};

class Field :  public Textured {
private:
    GLfloat width = 0;
    GLfloat height = 0;
    bool prepared = false;
    GLuint list_id;
public:
    Field(GLfloat width, GLfloat height) : Textured(){
        this->width = width;
        this->height = height;
    }
    GLfloat get_width() override{
        return this->width;
    }

    GLfloat get_height() override {
        return this->height;
    }

    void draw() override {
        if (this->texture->get_id()) {
            if (!prepared) {
                list_id = glGenLists(1);
                glNewList(list_id, GL_COMPILE);
                float ts_w = this->width/float(this->texture->get_width());
                float ts_h = this->width/float(this->texture->get_height());
                glBindTexture(GL_TEXTURE_2D, this->texture->get_id());
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

                glBegin(GL_QUADS);
                glTexCoord2f(0, 0);
                glVertex2f(0, 0);
                glTexCoord2f(0, ts_h);
                glVertex2f(0, this->height);
                glTexCoord2f(ts_w, ts_h);
                glVertex2f(this->width, this->height);
                glTexCoord2f(ts_w, 0);
                glVertex2f(this->width, 0);
                glEnd();
                glEndList();
            }
            glCallList(list_id);
        }
    }
};
